
using System;
using System.Collections.Generic;
using System.Linq;
using Xunit;
using Xunit.Extensions;


namespace Boilen.Primitives.Members {

    public class TestDoc {

        [Fact]
        public void GetDocTypeName_fails_for_null_type_name( ) {
            string nullTypeName = null;

            Assert.Throws<ArgumentNullException>( ( ) => Doc.GetDocTypeName( nullTypeName ) );
        }

        [Theory]
        [InlineData( "int", "int" )]
        [InlineData( "Exception", "Exception" )]
        [InlineData( "List<int>", "List{T}" )]
        [InlineData( "Dictionary<int, double>", "Dictionary{T0,T1}" )]
        [InlineData( "Tuple<int, Tuple<double>, Tuple<int, double>>", "Tuple{T0,T1,T2}" )]
        public void GetDocTypeName_returns_expected_value_when_using_numbered_generic_arguments( string typeName, string expectedDocTypeName ) {
            string docTypeName = Doc.GetDocTypeName( typeName );

            Assert.Equal( expectedDocTypeName, docTypeName );
        }

        [Theory]
        [InlineData( "int", "int" )]
        [InlineData( "Exception", "Exception" )]
        [InlineData( "List<int>", "List`1" )]
        [InlineData( "Dictionary<int, double>", "Dictionary`2" )]
        [InlineData( "Tuple<int, Tuple<double>, Tuple<int, double>>", "Tuple`3" )]
        public void GetDocTypeName_returns_expected_value_when_using_generic_argument_count( string typeName, string expectedDocTypeName ) {
            string docTypeName = Doc.GetDocTypeName( typeName, false );

            Assert.Equal( expectedDocTypeName, docTypeName );
        }

        [Theory]
        [InlineData( "int", "int" )]
        [InlineData( "Exception", "Exception" )]
        [InlineData( "List<int>", "List{int}" )]
        [InlineData( "Dictionary<int, double>", "Dictionary{int,double}" )]
        [InlineData( "Tuple<int, Tuple<double>, Tuple<int, double>>", "Tuple{int,Tuple{double},Tuple{int,double}}" )]
        public void GetDocTypeName_returns_expected_value_when_using_original_generic_arguments( string typeName, string expectedDocTypeName ) {
            string docTypeName = Doc.GetDocTypeName( typeName, null );

            Assert.Equal( expectedDocTypeName, docTypeName );
        }


        [Fact]
        public void constructor_fails_for_null_fully_qualified_name( ) {
            string name = "name";
            string nullName = null;
            string type = "type";
            string parentType = "parent";

            Assert.Throws<ArgumentNullException>( ( ) => new Doc( nullName, name, type, parentType ) );
        }

        [Fact]
        public void constructor_fails_for_empty_fully_qualified_name( ) {
            string name = "name";
            string emptyName = "";
            string type = "type";
            string parentType = "parent";

            Assert.Throws<ArgumentException>( ( ) => new Doc( emptyName, name, type, parentType ) );
        }

        [Fact]
        public void constructor_fails_for_null_name( ) {
            string name = "name";
            string nullName = null;
            string type = "type";
            string parentType = "parent";

            Assert.Throws<ArgumentNullException>( ( ) => new Doc( name, nullName, type, parentType ) );
        }

        [Fact]
        public void constructor_fails_for_empty_name( ) {
            string name = "name";
            string emptyName = "";
            string type = "type";
            string parentType = "parent";

            Assert.Throws<ArgumentException>( ( ) => new Doc( name, emptyName, type, parentType ) );
        }

        [Fact]
        public void constructor_fails_for_null_type( ) {
            string name = "name";
            string nullType = null;
            string parentType = "parent";

            Assert.Throws<ArgumentNullException>( ( ) => new Doc( name, name, nullType, parentType ) );
        }

        [Fact]
        public void constructor_fails_for_empty_type( ) {
            string name = "name";
            string emptyType = "";
            string parentType = "parent";

            Assert.Throws<ArgumentException>( ( ) => new Doc( name, name, emptyType, parentType ) );
        }

        [Theory]
        [InlineData( null )]
        [InlineData( "" )]
        [InlineData( "parent" )]
        public void constructor_succeeds_for_valid_arguments( string parentType ) {
            string name = "name";
            string type = "type";

            var doc = new Doc( name, name, type, parentType );

            Assert.Null( doc.InheritFrom );
        }


        [Fact]
        public void AddReplacement_fails_for_null_key( ) {
            string nullKey = null;
            string format = "format";
            var doc = CreateDoc( );

            Assert.Throws<ArgumentNullException>( ( ) => doc.AddReplacement( nullKey, format ) );
        }

        [Fact]
        public void AddReplacement_fails_for_empty_key( ) {
            string emptyKey = "";
            string format = "format";
            var doc = CreateDoc( );

            Assert.Throws<ArgumentException>( ( ) => doc.AddReplacement( emptyKey, format ) );
        }

        [Fact]
        public void AddReplacement_fails_for_null_format( ) {
            string key = "key";
            string nullFormat = null;
            var doc = CreateDoc( );

            Assert.Throws<ArgumentNullException>( ( ) => doc.AddReplacement( key, nullFormat ) );
        }

        [Fact]
        public void AddReplacement_succeeds_for_empty_key( ) {
            string key = "key";
            string emptyFormat = "";
            var doc = CreateDoc( );

            var returnedDoc = doc.AddReplacement( key, emptyFormat );

            Assert.Same( returnedDoc, doc );
        }

        [Fact]
        public void AddReplacement_succeeds_for_valid_values( ) {
            string key = "key";
            string format = "format";
            var doc = CreateDoc( );

            var returnedDoc = doc.AddReplacement( key, format );

            Assert.Same( returnedDoc, doc );
        }


        [Fact]
        public void AddExpander_fails_for_null_key( ) {
            string nullKey = null;
            string format = "format";
            var doc = CreateDoc( );

            Assert.Throws<ArgumentNullException>( ( ) => doc.AddExpander( nullKey, format ) );
        }

        [Fact]
        public void AddExpander_fails_for_empty_key( ) {
            string emptyKey = "";
            string format = "format";
            var doc = CreateDoc( );

            Assert.Throws<ArgumentException>( ( ) => doc.AddExpander( emptyKey, format ) );
        }

        [Fact]
        public void AddExpander_fails_for_null_format( ) {
            string key = "key";
            string nullFormat = null;
            var doc = CreateDoc( );

            Assert.Throws<ArgumentNullException>( ( ) => doc.AddExpander( key, nullFormat ) );
        }

        [Fact]
        public void AddExpander_succeeds_for_empty_key( ) {
            string key = "key";
            string emptyFormat = "";
            var doc = CreateDoc( );

            var returnedDoc = doc.AddExpander( key, emptyFormat );

            Assert.Same( returnedDoc, doc );
        }

        [Fact]
        public void AddExpander_succeeds_for_valid_values( ) {
            string key = "key";
            string format = "format";
            var doc = CreateDoc( );

            var returnedDoc = doc.AddExpander( key, format );

            Assert.Same( returnedDoc, doc );
        }


        [Fact]
        public void ApplyReplacements_fails_for_null_value( ) {
            string nullValue = null;
            var doc = CreateDoc( );

            Assert.Throws<ArgumentNullException>( ( ) => doc.ApplyReplacements( nullValue ) );
        }

        [Fact]
        public void ApplyReplacements_succeeds_for_escaped_expander( ) {
            string value = "ab%%c";
            string expectedValue = "ab%c";
            var doc = CreateDoc( );

            string appliedValue = doc.ApplyReplacements( value );

            Assert.Equal( appliedValue, expectedValue );
        }

        [Fact]
        public void ApplyReplacements_succeeds_for_multiple_expanders( ) {
            string value = "%see:AAA% returns %paramref:BBB%.";
            string expectedValue = "<see cref='AAA'/> returns <paramref name='BBB'/>.";
            var doc = CreateDoc( );

            string appliedValue = doc.ApplyReplacements( value );
            appliedValue = doc.ApplyReplacements( value );

            Assert.Equal( appliedValue, expectedValue );
        }

        [Theory]
        [InlineData( false, " null", " <see langword='null'/>" )]
        [InlineData( false, " true", " <see langword='true'/>" )]
        [InlineData( false, " false", " <see langword='false'/>" )]
        [InlineData( true, "custom replacement", "!!!!!" )]
        public void ApplyReplacements_succeeds_for_replacements( bool isCustom, string key, string replacementValue ) {
            string baseValue = " abc ";
            string value = baseValue + key + baseValue;
            string expectedValue = baseValue + replacementValue + baseValue;

            var doc = CreateDoc( );
            if( isCustom )
                doc.AddReplacement( key, replacementValue );

            string appliedValue = doc.ApplyReplacements( value );

            Assert.Equal( appliedValue, expectedValue );
        }

        [Theory]
        [InlineData( false, 0, Doc.Name, NameValue )]
        [InlineData( false, 0, Doc.Type, TypeValue )]
        [InlineData( false, 0, Doc.ParentType, ParentTypeValue )]
        [InlineData( false, 1, "see", "<see cref='{0}'/>" )]
        [InlineData( false, 1, "paramref", "<paramref name='{0}'/>" )]
        [InlineData( false, 1, "langword", "<see langword='{0}'/>" )]
        [InlineData( true, 0, "custom expander", "format" )]
        [InlineData( true, 1, "custom expander", "format {0}" )]
        [InlineData( true, 2, "custom expander", "format {0}" )]
        [InlineData( true, 2, "custom expander", "format {0} {1}" )]
        public void ApplyReplacements_succeeds_for_expanders( bool isCustom, int argCount, string key, string format ) {
            string[] args = Enumerable.Range( 0, argCount ).Select( i => "arg" + i ).ToArray( );
            string replacementValue = string.Format( format, args );

            string baseValue = " abc ";
            string expanderValue = string.Join( Doc.ExpanderArgumentsDelimiter.ToString( ), new[] { key }.Concat( args ).ToArray( ) );
            string value = string.Format( "{0}{1}{2}{1}{0}", baseValue, Doc.ExpanderDelimiter, expanderValue );
            string expectedValue = baseValue + replacementValue + baseValue;

            var doc = CreateDoc( );
            if( isCustom )
                doc.AddExpander( key, format );

            string appliedValue = doc.ApplyReplacements( value );

            Assert.Equal( appliedValue, expectedValue );
        }

        [Fact]
        public void ApplyReplacements_succeeds_for_nested_expanders( ) {
            string value = "Use %see:type.Method(int)%.";
            string expectedValue = "Use <see cref='" + TypeValue + ".Method(int)'/>.";
            var doc = CreateDoc( );

            string appliedValue = doc.ApplyReplacements( value );

            Assert.Equal( appliedValue, expectedValue );
        }


        [Fact]
        public void InheritFrom_outputs_nothing_when_assigned_null( ) {
            string[] expectedLines = StringBuilderCodeWriter.EmptyLines;
            var writer = new StringBuilderCodeWriter( );
            var doc = CreateDoc( );

            doc.InheritFrom = null;
            doc.Write( writer );

            string[] lines = writer.GetLines( );
            AssertExtensions.EqualCollection( lines, expectedLines );
        }

        [Fact]
        public void InheritFrom_outputs_inherit_element_when_assigned_empty_string( ) {
            string[] expectedLines = new[] { DocCommentPrefix + "<inheritdoc/>", "" };
            var writer = new StringBuilderCodeWriter( );
            var doc = CreateDoc( );

            doc.InheritFrom = "";
            doc.Write( writer );

            string[] lines = writer.GetLines( );
            AssertExtensions.EqualCollection( lines, expectedLines );
        }

        [Fact]
        public void InheritFrom_outputs_inherit_element_with_attribute_when_assigned_nonempty_string( ) {
            string inheritFrom = "value";
            string[] expectedLines = new[] { DocCommentPrefix + "<inheritdoc cref='" + inheritFrom + "'/>", "" };
            var writer = new StringBuilderCodeWriter( );
            var doc = CreateDoc( );

            doc.InheritFrom = inheritFrom;
            doc.Write( writer );

            string[] lines = writer.GetLines( );
            AssertExtensions.EqualCollection( lines, expectedLines );
        }


        [Fact]
        public void AddDocElement_fails_for_null_name( ) {
            string nullName = null;
            string value = "value";
            var doc = CreateDoc( );

            Assert.Throws<ArgumentNullException>( ( ) => doc.AddDocElement( nullName, value ) );
        }

        [Fact]
        public void AddDocElement_fails_for_empty_name( ) {
            string emptyName = "";
            string value = "value";
            var doc = CreateDoc( );

            Assert.Throws<ArgumentException>( ( ) => doc.AddDocElement( emptyName, value ) );
        }

        [Fact]
        public void AddDocElement_fails_for_empty_value( ) {
            string name = "name";
            string emptyValue = "";
            var doc = CreateDoc( );

            Assert.Throws<ArgumentException>( ( ) => doc.AddDocElement( name, emptyValue ) );
        }

        [Fact]
        public void AddDocElement_succeeds_for_valid_values_without_args( ) {
            string name = "name";
            string value = "value";
            var doc = CreateDoc( );

            var returnedDoc = doc.AddDocElement( name, value );

            Assert.Same( returnedDoc, doc );
            TestDocElementOutput( doc, name, value );
        }

        [Fact]
        public void AddDocElement_succeeds_for_valid_values_with_args( ) {
            string name = "name";
            string format = "value {0}";
            string arg = "arg";
            var doc = CreateDoc( );

            var returnedDoc = doc.AddDocElement( name, format, arg );

            Assert.Same( returnedDoc, doc );
            TestDocElementOutput( doc, name, format, arg );
        }

        [Theory]
        [PropertyData( "MultipleValueCounts" )]
        public void AddDocElement_succeeds_for_multiple_values( int count ) {
            string name = "name";
            string format = "value {0}";
            var doc = CreateDoc( );

            for( int i = 0; i < count; ++i ) {
                var returnedDoc = doc.AddDocElement( name, format, i );
                Assert.Same( returnedDoc, doc );
            }

            TestMultipleDocElementOutput( doc, name, format, null, count );
        }


        [Fact]
        public void AddSummary_succeeds_for_lookup( ) {
            string lookup = "%lookup:System.Windows.Controls.Panel.Children";
            var doc = CreateDoc( );

            var returnedDoc = doc.AddSummary( "Gets {0}.", lookup );

            Assert.Same( returnedDoc, doc );
        }

        [Fact]
        public void AddSummary_succeeds_for_valid_values( ) {
            string expectedName = "summary";
            string format = "value {0}";
            string arg = "arg";
            var doc = CreateDoc( );

            var returnedDoc = doc.AddSummary( format, arg );

            Assert.Same( returnedDoc, doc );
            TestDocElementOutput( doc, expectedName, format, arg );
        }

        [Theory]
        [PropertyData( "MultipleValueCounts" )]
        public void AddSummary_succeeds_for_multiple_values( int count ) {
            string name = "summary";
            string format = "value {0}";
            var doc = CreateDoc( );

            for( int i = 0; i < count; ++i ) {
                var returnedDoc = doc.AddSummary( format, i );
                Assert.Same( returnedDoc, doc );
            }

            TestMultipleDocElementOutput( doc, name, format, null, count );
        }


        [Fact]
        public void AddParam_succeeds_for_valid_values( ) {
            string paramName = "param";
            string expectedName = "param name='" + paramName + "'";
            string format = "value {0}";
            string arg = "arg";
            var doc = CreateDoc( );

            var returnedDoc = doc.AddParam( paramName, format, arg );

            Assert.Same( returnedDoc, doc );
            TestDocElementOutput( doc, expectedName, format, arg );
        }


        [Fact]
        public void AddException_succeeds_for_valid_values( ) {
            string exceptionTypeName = "ArgumentException";
            string expectedName = "exception cref='" + exceptionTypeName + "'";
            string format = "value {0}";
            string arg = "arg";
            var doc = CreateDoc( );

            var returnedDoc = doc.AddException( exceptionTypeName, format, arg );

            Assert.Same( returnedDoc, doc );
            TestDocElementOutput( doc, expectedName, format, arg );
        }

        [Theory]
        [PropertyData( "MultipleValueCounts" )]
        public void AddException_succeeds_for_multiple_values( int count ) {
            string exceptionTypeName = "ArgumentException";
            string expectedName = "exception cref='" + exceptionTypeName + "'";
            string format = "value {0}";
            var doc = CreateDoc( );

            for( int i = 0; i < count; ++i ) {
                var returnedDoc = doc.AddException( exceptionTypeName, format, i );
                Assert.Same( returnedDoc, doc );
            }

            TestMultipleDocElementOutput( doc, expectedName, format, "-or-", count );
        }


        #region Utility

        private const string DocCommentPrefix = "/// ";

        private const string FullyQualifiedNameValue = "fully-qualified name value";
        private const string NameValue = "name value";
        private const string TypeValue = "type value";
        private const string ParentTypeValue = "parent type value";


        public static IEnumerable<object[]> MultipleValueCounts {
            get {
                for( int i = 1; i <= 3; ++i )
                    yield return new object[] { i };
            }
        }


        private static Doc CreateDoc( ) {
            return new Doc( FullyQualifiedNameValue, NameValue, TypeValue, ParentTypeValue );
        }

        private static void TestDocElementOutput( Doc doc, string attributedName, string format, params object[] args ) {
            string name = attributedName.Split( ' ' )[0];
            string value = string.Format( format, args );
            string[] expectedLines = new[] {
                DocCommentPrefix + "<" + attributedName + ">",
                DocCommentPrefix + value,
                DocCommentPrefix + "</" + name + ">",
                ""
            };

            TestDocWriteOutput( doc, expectedLines );
        }

        private static void TestMultipleDocElementOutput( Doc doc, string attributedName, string format, string separator, int count ) {
            string name = attributedName.Split( ' ' )[0];
            var expectedLines = new List<string>( );
            expectedLines.Add( DocCommentPrefix + "<" + attributedName + ">" );
            for( int i = 0; i < count; ++i ) {
                string indent = "";
                if( i > 0 ) {
                    indent = "  ";
                    if( !string.IsNullOrEmpty( separator ) )
                        expectedLines.Add( string.Format( DocCommentPrefix + "<para>{0}</para>", separator ) );
                    expectedLines.Add( DocCommentPrefix + "<para>" );
                }

                string value = string.Format( format, i );
                expectedLines.Add( DocCommentPrefix + indent + value );

                if( i > 0 )
                    expectedLines.Add( DocCommentPrefix + "</para>" );
            }
            expectedLines.Add( DocCommentPrefix + "</" + name + ">" );
            expectedLines.Add( "" );

            TestDocWriteOutput( doc, expectedLines );
        }

        private static void TestDocWriteOutput( Doc doc, IEnumerable<string> expectedLines ) {
            var writer = new StringBuilderCodeWriter( );

            doc.Write( writer );

            string[] lines = writer.GetLines( );
            AssertExtensions.EqualCollection( lines, expectedLines );
        }

        #endregion

    }

}
